// **********************************************************************
//
// <copyright>
//
//  BBN Technologies
//  10 Moulton Street
//  Cambridge, MA 02138
//  (617) 873-8000
//
//  Copyright (C) BBNT Solutions LLC. All rights reserved.
//
// </copyright>
// **********************************************************************
//
// $Source: /cvs/distapps/openmap/src/openmap/com/bbn/openmap/gui/OverviewMapHandler.java,v $
// $RCSfile: OverviewMapHandler.java,v $
// $Revision: 1.15 $
// $Date: 2006/08/09 21:08:41 $
// $Author: dietrick $
//
// **********************************************************************

package com.bbn.openmap.gui;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Insets;
import java.awt.Paint;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.geom.Point2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.Serializable;
import java.net.URL;
import java.util.Properties;
import java.util.Vector;

import javax.swing.ImageIcon;
import javax.swing.JButton;

import com.bbn.openmap.BufferedMapBean;
import com.bbn.openmap.Environment;
import com.bbn.openmap.Layer;
import com.bbn.openmap.LayerHandler;
import com.bbn.openmap.MapBean;
import com.bbn.openmap.MapHandler;
import com.bbn.openmap.PropertyConsumer;
import com.bbn.openmap.event.DefaultOverviewMouseMode;
import com.bbn.openmap.event.LayerEvent;
import com.bbn.openmap.event.ListenerSupport;
import com.bbn.openmap.event.MapMouseMode;
import com.bbn.openmap.event.OverviewMapStatusListener;
import com.bbn.openmap.event.ProjectionEvent;
import com.bbn.openmap.event.ProjectionListener;
import com.bbn.openmap.layer.OverviewMapAreaLayer;
import com.bbn.openmap.proj.Length;
import com.bbn.openmap.proj.Mercator;
import com.bbn.openmap.proj.Proj;
import com.bbn.openmap.proj.ProjMath;
import com.bbn.openmap.proj.Projection;
import com.bbn.openmap.proj.ProjectionFactory;
import com.bbn.openmap.util.ComponentFactory;
import com.bbn.openmap.util.Debug;
import com.bbn.openmap.util.PropUtils;

/**
 * The OverviewMapHandler contains a MapBean that contains a projection that
 * reflects another MapBean's projection. It manages the two MapBeans and the
 * differences in the projections betwen them. The OverviewMapHandler can have a
 * projection type independent of that of the source MapBean (the MapBean that
 * the OverviewMapHandler's MapBean is paying attention to). It also contains a
 * scale factor, which is a multiplier to use against the scale of the source
 * MapBean's scale.
 * <P>
 * 
 * The OverviewMapHandler MapBean can also be used to control the source
 * MapBean's projection center and scale. The source MapBean just needs to be
 * added to the OverviewMapHandler by
 * OverviewMapHandler.addControlledMap(MapBean).
 * <P>
 * 
 * The OverviewMapHandler needs to be added to the source MapBean as a
 * ProjectionListener. Then, the overview MapBean can be added to the
 * ContentPane of a Component by calling
 * Component.setContentPane(OverviewMapHandler.getMap()); The OverviewMapHandler
 * Should also be added as a ComponentListener to the Component.
 * <P>
 * 
 * After the first projectionChanged() call is received, the OverviewMapHandler
 * knows about the source MapBean. Since the OverviewMapHandler is a
 * ComponentListener and will therefore find out when it's parent is hidden, it
 * will disengage and engage itself from the source MapBean as it's visibility
 * changes.
 * <P>
 * 
 * To get the overview map to appear in the OpenMap application, add the
 * following properties to your openmap.properties file:
 * 
 * <pre>
 * 
 * 
 *       # First, add overviewMapHandler to the openmap.components marker name list.  Then, add:
 *     
 *       overviewMapHandler.class=com.bbn.opemap.gui.OverviewMapHandler
 *       overviewMapHandler.overviewLayers=overviewLayer
 *       overviewMapHandler.overviewScaleFactor=10f
 *       overviewMapHandler.overviewMinScale=100f
 *       # Set the Unit of Measure for the overviewMinScale property
 *       # Omission of  overviewMinScaleUom indicates that overviewMinScale
 *       # is an projection map scale instead of a distance.
 *       overviewMapHandler.overviewMinScaleUom=km
 *     
 *       # 'overviewStatusLayer' is a marker name for any attributes you may
 *       # want to pass to the overviewStatusLayer instance, in addition to
 *       # being used to define the class to use for that special layer.
 *       overviewMapHandler.overviewStatusLayer.class=com.bbn.openmap.layer.OverviewMapAreaLayer
 *       # Properties can be passed to the overview status layer by listing
 *       # them with the OverviewMapHandler prefix.
 *     
 *       # Set the line color for the coverage box outline...
 *       # overviewMapHandler.lineColor=FFFF0000
 *     
 *       # A sample overview map layer
 *       overviewLayer.class=com.bbn.openmap.layer.shape.ShapeLayer
 *       overviewLayer.prettyName=Overview
 *       overviewLayer.shapeFile=/home/dietrick/dev/openmap/share/dcwpo-browse.shp
 *       overviewLayer.spatialIndex=/home/dietrick/dev/openmap/share/dcwpo-browse.ssx
 *       overviewLayer.lineColor=ff000000
 *       overviewLayer.fillColor=ffbdde83
 * 
 * 
 * </pre>
 * 
 * <p>
 * 
 * If layers are not added to the overview map, then it won't show up in the
 * application.
 */
public class OverviewMapHandler extends OMToolComponent implements ProjectionListener,
        Serializable, PropertyConsumer, PropertyChangeListener, ComponentListener {

    public final static String OverviewMapHandlerLayerProperty = "overviewLayers";
    public final static String ScaleFactorProperty = "overviewScaleFactor";
    public final static String ProjectionTypeProperty = "overviewProjectionType";
    public final static String MinScaleProperty = "overviewMinScale";
    public final static String MinScaleUomProperty = "overviewMinScaleUom";
    public final static String StatusLayerProperty = "overviewStatusLayer";
    public final static String ControlSourceMapProperty = "overviewControlSourceMap";
    public final static String BackgroundSlaveProperty = "backgroundSlave";
    public final static float defaultScaleFactor = 20f;
    public final static float defaultMinScale = 500000f;
    public final static Length defaultMinScaleUom = null;

    /** The multiplier to apply to the scale of the project received. */
    protected float scaleFactor;
    /**
     * The minimum scale to use for the window. If it gets too small with a
     * general type layer, it won't be any use.
     */
    protected float minScale;

    /**
     * The minimum scale unit of measure to use for the window. Use during
     * initialization to compute a projection scale from a distance.
     */
    protected Length minScaleUom;

    /** The map of the overview panel. */
    protected transient MapBean map;
    /**
     * The source MapBean to show the overview of. Gets set when the first
     * projectionChanged() gets called. Also used to disconnect from the MapBean
     * when the component that this OverviewMapHandler is listening to is
     * hidden, and to connect to the MapBean when the component is shown.
     */
    protected transient MapBean sourceMap;
    /** The projection of the overview map bean. */
    protected transient Proj projection;
    /**
     * A layer that can be set to constantly be on the top of the map. If the
     * status layer is also a OverviewMapStatusListener, it also receives the
     * source map projection when that changes, which gives it the capability to
     * draw stuff based on that.
     */
    protected Layer statusLayer;
    /**
     * The support to send the source MapBean setCenter and setScale commands if
     * a controlled map is added - usually the source map bean.
     */
    protected transient ControlledMapSupport controlledMaps;
    /** The mouse mode to use for the overview map. */
    protected MapMouseMode mmm;
    /**
     * The thing listening for a request to bring up a JFrame or JInternalFrame.
     */
    protected ActionListener overviewFrameActionListener = null;
    /** Indicates if OverviewMap should be controlling sourceMap. */
    protected boolean controlSourceMap = true;
    /** Default Frame title for OverviewMapHandler */
    public static final String defaultFrameTitle = "Overview Map";
    /** String The Frame Title */
    protected String frameTitle = defaultFrameTitle;
    /** Default key for Tool */
    public static final String defaultKey = "overviewmaphandler";

    /**
     * Flag to change the background color to whatever the source map's is
     * changed to. True by default.
     */
    protected boolean backgroundSlave = true;

    public final static int INITIAL_WIDTH = 200;
    public final static int INITIAL_HEIGHT = 100;

    /**
     * Default constructor. make sure init(someProperties) is called before you
     * attempt to use this object
     */
    public OverviewMapHandler() {
        super();
        setKey(defaultKey);
        setLayout(new BorderLayout());
        createOverviewMap();

        // Set up a default...
        projection = createStartingProjection(null);
        addComponentListener(this);
        // Create this when we need it.
        // setWindowSupport(new WindowSupport(this, new WindowSupport.Dlg(null,
        // "Overview Map")));
    }

    /**
     * Create an OverviewMapHandler with properties that do not contain a
     * prefix.
     * 
     * @param props properties object.
     */
    public OverviewMapHandler(Properties props) throws Exception {
        this(null, props);
    }

    /**
     * Create an OverviewMapHandler with properties that do contain a prefix.
     * 
     * @param prefix the prefix for all the properties that apply to the
     *        OverviewMapHandler.
     * @param props properties object.
     */
    public OverviewMapHandler(String prefix, Properties props) throws Exception {

        this();
        setProperties(prefix, props);
    }

    /**
     * Create an OverviewMapHandler for given MapBean.
     * 
     * @param srcMap srcMapBean
     * @param prefix the prefix to place in front of each property - i.e., so
     *        that each property will be under prefix.propertyName. The period
     *        between the two will be added.
     * @param props properties object.
     */
    public OverviewMapHandler(MapBean srcMap, String prefix, Properties props) throws Exception {
        this(prefix, props);
        setSourceMap(srcMap);
    }

    /**
     * Create the MapBean used for the overview map, and suppress the copyright
     * message at the same time.
     */
    protected void createOverviewMap() {
        // We don't need another copyright message, right?
        MapBean.suppressCopyright = true;
        map = new BufferedMapBean();
        this.add(map, BorderLayout.CENTER);
    }

    /**
     * Initialize based on properties, which will not have a prefix.
     * 
     * @deprecated use setProperties(props).
     */
    public void init(Properties props) throws Exception {
        setProperties(null, props);
    }

    /**
     * Initialize an OverviewMapHandler with properties that do contain a
     * prefix.
     * 
     * @param prefix the prefix to place in front of each property - i.e., so
     *        that each property will be under prefix.propertyName. The period
     *        between the two will be added.
     * @param props properties object.
     * @deprecated use setProperties(prefix, props).
     */
    public void init(String prefix, Properties props) throws Exception {
        setProperties(prefix, props);
    }

    /**
     * Sets the properties for the <code>Layer</code>. This allows
     * <code>Layer</code> s to get a richer set of parameters than the
     * <code>setArgs</code> method. Part of the PropertyConsumer interface.
     * Layers which override this method should do something like: 
     * <p>
     * <pre>
     * public void setProperties(String prefix, Properties props) {
     *     super.setProperties(prefix, props);
     *     // do local stuff
     * }
     * </pre>
     * <p> If the addToBeanContext property is not defined, it is set
     * to false here.
     * 
     * @param prefix the token to prefix the property names
     * @param props the <code>Properties</code> object
     */
    public void setProperties(String prefix, java.util.Properties props) {
        propertyPrefix = prefix;

        prefix = PropUtils.getScopedPropertyPrefix(prefix);

        Vector<String> overviewLayers = PropUtils.parseSpacedMarkers(props.getProperty(prefix
                + OverviewMapHandlerLayerProperty));

        if (overviewLayers.isEmpty()) {
            Debug.message("overview", "OverviewMapHandler:  created without layers!");
        }

        scaleFactor = PropUtils.floatFromProperties(props, prefix + ScaleFactorProperty, defaultScaleFactor);

        minScale = PropUtils.floatFromProperties(props, prefix + MinScaleProperty, defaultMinScale);

        String uom = props.getProperty(prefix + MinScaleUomProperty);
        if (uom != null) {
            minScaleUom = Length.get(uom);
            setMinScale(minScale, minScaleUom);
        }

        backgroundSlave = PropUtils.booleanFromProperties(props, prefix + BackgroundSlaveProperty, backgroundSlave);

        setControlSourceMap(PropUtils.booleanFromProperties(props, prefix
                + ControlSourceMapProperty, controlSourceMap));

        String statusLayerName = props.getProperty(prefix + StatusLayerProperty + ".class");
        if (statusLayerName != null) {
            statusLayer = (Layer) ComponentFactory.create(statusLayerName, prefix
                    + StatusLayerProperty, props);
            if (statusLayer == null) {
                Debug.error("OverviewMapHandler.setProperties: status layer not set.");
            }
        } else {
            statusLayer = new OverviewMapAreaLayer();
        }

        statusLayer.setProperties(prefix, props);

        projection = createStartingProjection(props.getProperty(prefix + ProjectionTypeProperty));

        setLayers(LayerHandler.getLayers(overviewLayers, overviewLayers, props));
    }

    protected ProjectionFactory getProjectionFactory() {
        if (sourceMap != null) {
            return sourceMap.getProjectionFactory();
        }
        return ProjectionFactory.loadDefaultProjections();
    }

    private Proj createStartingProjection(String projName) {

        ProjectionFactory projectionFactory = getProjectionFactory();
        Class<? extends Projection> projClass = projectionFactory.getProjClassForName(projName);

        if (projClass == null) {
            projClass = Mercator.class;
        }

        // The scale, lat/lon and size shouldn't matter, because the
        // size will get reset when it is added to a component, and
        // the projection will change when it is added to a MapBean
        // as a projection listener.p
        return (Proj) projectionFactory.makeProjection(projClass, new Point2D.Float(Environment.getFloat(Environment.Latitude, 0f), Environment.getFloat(Environment.Longitude, 0f)), Environment.getFloat(Environment.Scale, Float.POSITIVE_INFINITY)
                * scaleFactor, INITIAL_WIDTH, INITIAL_HEIGHT);
    }

    /**
     * PropertyConsumer method, to fill in a Properties object, reflecting the
     * current values of the layer. If the layer has a propertyPrefix set, the
     * property keys should have that prefix plus a separating '.' prepended to
     * each property key it uses for configuration.
     * 
     * @param props a Properties object to load the PropertyConsumer properties
     *        into. If props equals null, then a new Properties object should be
     *        created.
     * @return Properties object containing PropertyConsumer property values. If
     *         getList was not null, this should equal getList. Otherwise, it
     *         should be the Properties object created by the PropertyConsumer.
     */
    public Properties getProperties(Properties props) {
        if (props == null) {
            props = new Properties();
        }

        String prefix = PropUtils.getScopedPropertyPrefix(this);

        // Build marker list
        StringBuffer layerList = new StringBuffer();
        Component[] comps = map.getComponents();
        int ncomponents = comps.length;
        for (int i = 0; i < ncomponents; i++) {
            Layer layer = (Layer) comps[i];
            if (layer != statusLayer) { // Take care of the
                // statusLayer later.
                layerList.append(" ").append(layer.getPropertyPrefix());
                layer.getProperties(props);
            }
        }
        props.put(prefix + OverviewMapHandlerLayerProperty, layerList.toString());

        props.put(prefix + ScaleFactorProperty, Float.toString(scaleFactor));
        props.put(prefix + ProjectionTypeProperty, map.getProjection().getName());
        props.put(prefix + MinScaleProperty, Float.toString(minScale));
        props.put(prefix + BackgroundSlaveProperty, new Boolean(backgroundSlave).toString());

        if (statusLayer != null) {
            props.put(prefix + StatusLayerProperty, statusLayer.getClass().getName());
            statusLayer.getProperties(props);
        }

        props.put(prefix + ControlSourceMapProperty, new Boolean(controlSourceMap).toString());

        return props;
    }

    /**
     * Method to fill in a Properties object with values reflecting the
     * properties able to be set on this PropertyConsumer. The key for each
     * property should be the raw property name (without a prefix) with a value
     * that is a String that describes what the property key represents, along
     * with any other information about the property that would be helpful
     * (range, default value, etc.). For Layer, this method should at least
     * return the 'prettyName' property.
     * 
     * @param list a Properties object to load the PropertyConsumer properties
     *        into. If getList equals null, then a new Properties object should
     *        be created.
     * @return Properties object containing PropertyConsumer property values. If
     *         getList was not null, this should equal getList. Otherwise, it
     *         should be the Properties object created by the PropertyConsumer.
     */
    public Properties getPropertyInfo(Properties list) {
        if (list == null) {
            list = new Properties();
        }

        list.put(OverviewMapHandlerLayerProperty, "Space separated list of marker names of layers to use as background on the overview map.");
        list.put(ScaleFactorProperty, "Multiplier reflecting the difference between the scale of the overview map and the source map (default is 20.0).");
        list.put(ProjectionTypeProperty, "Projection name to use for overview map (Default is mercator).");
        list.put(MinScaleProperty, "Minimum scale of overview map (Default is 500,000.0).");
        list.put(StatusLayerProperty, "Class name of layer to use as the active layer on the overview map, receiving mouse events (Default is com.bbn.openmap.layer.OverviewMapAreaLayer).");
        list.put(ControlSourceMapProperty, "Flag to have the source map controlled by gestures on the overview map (true/false, default is true).");
        list.put(ControlSourceMapProperty + ScopedEditorProperty, "com.bbn.openmap.util.propertyEditor.TrueFalsePropertyEditor");
        list.put(BackgroundSlaveProperty, "Flag to have the map mimic any changes made to the source map's background (true/false, default is true).");
        list.put(BackgroundSlaveProperty + ScopedEditorProperty, "com.bbn.openmap.util.propertyEditor.TrueFalsePropertyEditor");

        statusLayer.getPropertyInfo(list);

        return list;
    }

    /**
     * Sets the sourceMap associated with this OverviewMap. if controlSourceMap
     * property is set, srcMap will also be controlled by this OverviewHandler
     * Passing a null value will remove the current sourceMap from the list of
     * Maps that this handler is controlling and set sourceMap to null.
     * 
     * @param srcMap the master map.
     */
    public void setSourceMap(MapBean srcMap) {
        if (sourceMap != null) {
            removeControlledMap(sourceMap);
            sourceMap.removeProjectionListener(this);
            sourceMap.removePropertyChangeListener(this);
        }

        // Add the sourceMap to a set of listeners that wish to be
        // controlled by this OverviewMapHandler
        if (srcMap != null) {
            if (controlSourceMap == true) {
                addControlledMap(srcMap);
            }

            // Check and see if the overview map window is up. If it
            // is, we should add the overview map as a projection
            // listener to it. Note: overview map windows went away
            // due to window support, but not sure how it affected
            // this statement. Keeping commented code here for
            // reference in case behavior is weird. Seems to be
            // working as expected, though. DFD
            // if ((overviewWindowFrame != null &&
            // overviewWindowFrame.isShowing()) ||
            // (overviewWindow != null && overviewWindow.isShowing())
            // ||

            // Turns out non-tool overview maps weren't becoming
            // projection change listeners...
            if (!getUseAsTool() && isVisible()) {
                srcMap.addProjectionListener(this);
            }

            srcMap.addPropertyChangeListener(this);
        }

        sourceMap = srcMap;
    }

    /**
     * Get the map that the overview map is listening to.
     */
    public MapBean getSourceMap() {
        return sourceMap;
    }

    /**
     * Set the string used for the frame title of the overview map.
     */
    public void setFrameTitle(String in_frameTitle) {
        frameTitle = in_frameTitle;
    }

    public String getFrameTitle() {
        return frameTitle;
    }

    /**
     * Set whether the map's background mimics changes to the source map's
     * background.
     * 
     * @param set true to enable mimicking.
     */
    public void setBackgroundSlave(boolean set) {
        backgroundSlave = set;
    }

    /**
     * Get whether the map's background mimics changes to the source map's
     * background.
     */
    public boolean getBackgroundSlave() {
        return backgroundSlave;
    }

    /**
     * Default value of this property is true. if you want your sourceMap to be
     * controlled by this OverviewMapHandler, set the value of this property for
     * this OverviewHandler. This will allow, for instance, clicking on the
     * overview map to recenter the source map. That depends on the overview map
     * mouse mode, however.
     * 
     * @param value
     */
    public void setControlSourceMap(boolean value) {
        if (sourceMap != null) {
            if (value == true && controlSourceMap == false) {
                addControlledMap(sourceMap);
            }
            if (value == false && controlSourceMap == true) {
                removeControlledMap(sourceMap);
            }
        }
        controlSourceMap = value;
    }

    public boolean getControlSourceMap() {
        return controlSourceMap;
    }

    /**
     * Set the layers in the Overview MapBean. An AreaLayer is automatically
     * added on top.
     */
    public void setLayers(Layer[] layers) {
        map.setLayers(new LayerEvent(this, LayerEvent.REPLACE, new Layer[0]));
        if (statusLayer != null) {
            map.add(statusLayer);
        }
        map.setLayers(new LayerEvent(this, LayerEvent.ADD, layers));
    }

    /**
     * Part of the ProjectionListener interface. The new projections from the
     * source MapBean arrive here.
     * 
     * @param projEvent the projection event from the source MapBean.
     */
    public void projectionChanged(ProjectionEvent projEvent) {
        if (sourceMap == null) {
            sourceMap = (MapBean) projEvent.getSource();
            map.setBckgrnd(sourceMap.getBckgrnd());
        }

        Projection proj = projEvent.getProjection();
        if (proj == null) {
            return;
        }

        if (statusLayer instanceof OverviewMapStatusListener) {
            ((OverviewMapStatusListener) statusLayer).setSourceMapProjection(proj);
        }

        float newScale = proj.getScale() * scaleFactor;

        // Adjust the newScale based on the ratio of the
        // source projection width and the overview
        // map projection width.
        Projection sourceProj = sourceMap.getProjection();
        newScale *= (float) sourceProj.getWidth() / (float) projection.getWidth();

        if (newScale < minScale) {
            newScale = minScale;
        }

        projection.setScale(newScale);
        projection.setCenter(proj.getCenter());
        map.setProjection(projection);
    }

    /**
     * Set the MapMouseMode for the overview map. If you want the status layer
     * to listen to the mouse mode, you have to get the layer and wire it up
     * yourself.
     */
    public void setMouseMode(MapMouseMode ammm) {
        // If we're removing a mouse mode, disconnect it from the map.
        if (ammm == null) {
            deactivateMouseMode();
        }
        mmm = ammm;
        activateMouseMode();
    }

    /**
     * Get the MapMouseMode used for the overview map.
     */
    public MapMouseMode getMouseMode() {
        return mmm;
    }

    /**
     * Adds the mouse mode as a listener to the overview map. If the mouse mode
     * is null, the default is created.
     */
    public void activateMouseMode() {
        if (mmm == null) {
            mmm = new DefaultOverviewMouseMode(this);
        }
        if (map != null) {
            map.addMouseListener(mmm);
            map.addMouseMotionListener(mmm);
        }
    }

    /**
     * Disconnects the mouse mode from the overview map.
     */
    public void deactivateMouseMode() {
        if (mmm != null) {
            map.removeMouseListener(mmm);
            map.removeMouseMotionListener(mmm);
        }
    }

    /**
     * Add a controlled MapBean to the OverviewMapHandler. Use this method to
     * add another MapBean to the overview map in order to have its projection
     * controlled by the overview panel. If the overview panel is clicked on,
     * the listening MapBean will be recentered. If a box is drawn with a mouse
     * drag, the scale of the controlled map will be modified.
     * 
     * @param l MapBean.
     */
    public void addControlledMap(MapBean l) {
        if (l != null) {
            if (controlledMaps == null) {
                controlledMaps = new ControlledMapSupport(map);
                // If nobody has been listening don't draw anything.
                // Since someone is now being controlled, we'll do the
                // drawing.
                activateMouseMode();
            }
            controlledMaps.add(l);
        }
    }

    /**
     * Remove a controlled MapBean from the OverviewMapHandler.
     * 
     * @param l a MapBean.
     */
    public void removeControlledMap(MapBean l) {
        if (controlledMaps != null) {
            controlledMaps.remove(l);

            if (controlledMaps.isEmpty()) {
                deactivateMouseMode();
            }
        }
    }

    /**
     * Get the overview MapBean.
     * 
     * @return overview MapBean.
     */
    public MapBean getMap() {
        return map;
    }

    /**
     * Set the overview MapBean.
     */
    public void setMap(MapBean map) {
        if (map != null) {
            // get rid of any other MapBean that may have been added
            // to the JPanel.
            this.remove(map);
        }
        this.map = map;
        this.add(map, BorderLayout.CENTER);
    }

    /**
     * Get the ControlledMapSupport, which usually contains the source map.
     */
    public ControlledMapSupport getControlledMapListeners() {
        return controlledMaps;
    }

    /**
     * Set the ControlledMapSupport, which usually contains the source map.
     */
    public void setControlledMapListeners(ControlledMapSupport list) {
        controlledMaps = list;
    }

    /**
     * Get the status layer, which is always drawn on top of the other layers,
     * and maintained separately from other layers.
     */
    public Layer getStatusLayer() {
        return statusLayer;
    }

    /**
     * Get the status layer, which is always drawn on top of the other layers,
     * and maintained separately from other layers. If the layer is also an
     * OverviewMapStatusListener, it will receive source map projection changes,
     * so it can draw stuff on itself representing what's going on the source
     * map.
     */
    public void setStatusLayer(Layer layer) {
        statusLayer = layer;
    }

    /**
     * Set the scale factor to use between the source MapBean and the overview
     * MapBean. It's a direct multiplier, so the overview MapBean can actually
     * be a magnified map, too. The overview map scale = source MapBean scale *
     * scaleFactor.
     * 
     * @param setting scale factor
     */
    public void setScaleFactor(float setting) {
        scaleFactor = setting;
    }

    /**
     * Get the scale factor used for the overview MapBean.
     */
    public float getScaleFactor() {
        return scaleFactor;
    }

    /**
     * Set the projection of the overview MapBean. Lets you set the type,
     * really. The scale and center will be reset when a projection event is
     * received.
     */
    public void setProjection(Proj proj) {
        projection = proj;
    }

    /**
     * Get the current projection of the overview MapBean.
     */
    public Proj getProjection() {
        return projection;
    }

    /**
     * Set the minimum scale to use for the overview map. If this is set too
     * small with a very general map layer, it won't be of any use, really, if
     * it gets really zoomed in.
     * 
     * @param setting the scale setting - 1:setting
     */
    public void setMinScale(float setting) {
        if (setting > 0) {
            minScale = setting;
        }
    }

    /**
     * Set the minimum scale to use for the overview map. If this is set too
     * small with a very general map layer, it won't be of any use, really, if
     * it gets really zoomed in.
     * 
     * @param width setting the scale setting - 1:setting
     * @param uom unit of measure for width
     */
    public void setMinScale(float width, Length uom) {
        if (width > 0) {
            Projection p = map.getProjection();

            Length projUom = p.getUcuom();

            if (projUom == null) {
                // We're going to assume meters, since this really only applies
                // to pre-projected projections like Cartesian, and we might as
                // well make it something. Not sure if it matters, as long as
                // we're consistent.
                projUom = Length.METER;
            }

            // This is really not the radius but the half width in radians.
            float radius = uom.toRadians(width) / 2;

            Point2D center = p.getCenter();
            Point2D left = new Point2D.Double(center.getX(), center.getY());
            Point2D right = new Point2D.Double(center.getX(), center.getY());

            double newLeftX = projUom.fromRadians(projUom.toRadians(left.getX()) - radius);
            double newRightX = projUom.fromRadians(projUom.toRadians(right.getX()) + radius);

            left.setLocation(newLeftX, left.getY());
            right.setLocation(newRightX, right.getY());

            minScale = ProjMath.getScale(left, right, p);
        }
    }

    public float getMinScale() {
        return minScale;
    }

    /**
     * Invoked when component has been shown. This component should be the
     * component that contains the OverviewMapHandler.
     */
    public void componentShown(ComponentEvent e) {
        if (sourceMap != null) {
            sourceMap.addProjectionListener(this);
        }
    }

    /**
     * Invoked when component has been hidden. This component should be the
     * component that contains the OverviewMapHandler.
     */
    public void componentHidden(ComponentEvent e) {
        if (sourceMap != null) {
            sourceMap.removeProjectionListener(this);
        }
    }

    public void componentResized(ComponentEvent e) {
    }

    public void componentMoved(ComponentEvent e) {
    }

    /**
     * Return an ActionListener that will bring up an independent window with an
     * Overview Map.
     * 
     * @return ActionListener that brings up a Window when an actionPerformed is
     *         called.
     */
    public ActionListener getOverviewFrameActionListener() {
        return new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
                WindowSupport ws = getWindowSupport();

                MapHandler mh = (MapHandler) getBeanContext();
                Frame frame = null;
                if (mh != null) {
                    frame = (Frame) mh.get(java.awt.Frame.class);
                }

                if (ws == null) {
                    ws = new WindowSupport(OverviewMapHandler.this, new WindowSupport.Dlg(frame, "Overview Map"));
                    setWindowSupport(ws);
                }

                int w = INITIAL_WIDTH;
                int h = INITIAL_HEIGHT;
                Dimension dim = ws.getComponentSize();
                if (map != null && dim != null) {
                    w = (int) dim.getWidth();
                    h = (int) dim.getHeight();
                }

                ws.displayInWindow(frame, -1, -1, w, h);
            }
        };
    }

    /** Tool interface method. */
    public Container getFace() {
        JButton b = null;
        if (getUseAsTool()) {
            URL url = getClass().getResource("overview.gif");
            b = new JButton(new ImageIcon(url, frameTitle));
            b.setToolTipText(frameTitle);
            b.setMargin(new Insets(0, 0, 0, 0));
            b.addActionListener(getOverviewFrameActionListener());
            b.setBorderPainted(false);
        }
        return b;
    }

    /**
     * Called when the OverviewMapHandler is added to the BeanContext, and
     * whenever an object is added to the BeanContext after that. The
     * OverviewMapHandler looks for a MapBean to use as a source map, and for a
     * PropertiesHandler object to use to load itself with layers and other
     * properties. If a source MapBean is already set and another MapBean is
     * found, the last MapBean will be used as the source MapBean. Every time a
     * PropertyHandler is found, the OverviewMapHandler will reinitialize
     * itself.
     * 
     * @param someObj the object being added to the BeanContext
     */
    public void findAndInit(Object someObj) {
        if (someObj instanceof com.bbn.openmap.MapBean) {
            Debug.message("overview", "OverviewMapHandler found a MapBean object");
            setSourceMap((MapBean) someObj);
        }
    }

    /**
     */
    public void findAndUndo(Object someObj) {
        if (someObj instanceof MapBean) {
            if (getSourceMap() == (MapBean) someObj) {
                Debug.message("overview", "OverviewMapHandler: removing source MapBean");
                setSourceMap(null);
            }
        }

        if (someObj.equals(this)) {
            dispose();
        }
    }

    /**
     * Support for directing the setCenter and setScale calls to any MapBeans
     * that care to be listening.
     */
    public class ControlledMapSupport extends ListenerSupport<MapBean> {

        /**
         * Construct a ControlledMapSupport.
         * 
         * @param aSource source Object
         */
        public ControlledMapSupport(Object aSource) {
            super(aSource);
        }

        /**
         * Set the center coordinates on all registered listeners.
         * 
         * @param llp the new center point
         */
        public void setCenter(Point2D llp) {
            for (MapBean mapBean : this) {
                mapBean.setCenter(llp);
            }
        }

        /**
         * Set the scale on all registered listeners.
         * 
         * @param scale the new scale
         */
        public void setScale(float scale) {
            for (MapBean mapBean : this) {
                mapBean.setScale(scale);
            }
        }
    }

    /**
     * PropertyChangeListener method, to listen for the source map's background
     * changes. Act on if necessary.
     */
    public void propertyChange(PropertyChangeEvent pce) {
        if (pce.getPropertyName() == MapBean.BackgroundProperty && backgroundSlave) {
            map.setBckgrnd((Paint) pce.getNewValue());
        }
    }

    public void dispose() {
        controlledMaps.clear();
        map.dispose();
    }
}